/*
 Copyright (c) 2009 by contributors:

 * James Hight (http://labs.zavoo.com/)
 * Richard R. Masters

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
*/

package org.svgweb.core;

	import flash.xml.XML;
	import haxe.Timer;
    import org.svgweb.core.SVGNode;
    import org.svgweb.nodes.SVGSVGNode;
    import org.svgweb.nodes.SVGVideoNode;
    import org.svgweb.utils.SVGUnits;
    import org.svgweb.events.SVGEvent;
    import org.svgweb.smil.TimeSpec;
    import org.svgweb.smil.EventTimeSpec;
    import org.svgweb.smil.IndefiniteTimeSpec;
    import org.svgweb.smil.OffsetTimeSpec;
    import org.svgweb.smil.WallclockTimeSpec;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.net.URLRequest;
    //import flash.utils.getTimer;
	import me.cunity.tools.ArrayTools;

    /**
     *
     *  SVGTimedNode is a node with timing parameters.
     *
     *  The timing model is implemented according to SMIL Animation:
     *  http://www.w3.org/TR/smil-animation/
     *
     *
     *  SVGTimedNode has parameters which specify TimeSpecs (time specifications)
     *  (begin times or event to begin on, end times or events, etc).
     * 
     *  An instance list of begin times and end times is maintained to handle
     *  keeping track of instances created from static configuration and
     *  dynamic events.
     *
     *  Begin TimeSpecs are static time offsets or patterns that match events.
     *  begin instances are created either when parsed [static offsets ('5s')]
     *  or when events occur [timebase or syncbase time specs (e.g. 'foo.begin',
     *  'bar.click')].
     *
     *  The SVGTimedNode progresses along when it receives events of type
     *  _SVGDocTimeUpdate which indicates the timeline has progressed forward to a
     *  new timestamp (the next frame). At this point, events are fired for transitions
     *  between states, until no further transitions are left to perform up
     *  to the current time. The begin and end times are removed from the
     *  instance lists as they are processed.
     *
     *
     *
     **/
    class SVGTimedNode extends SVGNode {
        
        var INDEFINITE:Float ;

        var lastDocTime:Float ;

        var currentRepeatIndex:Int ;
        var eachRepeatDuration:Float;    // dur
        var repeatCount:Float;           // repeatCount
        var repeatCountSpecified:Bool ;
        var allRepeatsDuration:Float;    // repeatDuration
        var repeatDurSpecified:Bool ;
        var minIntervalDuration:Float;   // min
        var maxIntervalDuration:Float;   // max

        var beginTimeSpecs:Array<Dynamic> ;        // begin
        var endTimeSpecs:Array<Dynamic> ;          // end

        var beginTimes:Array<Dynamic> ;    // begin time instances
        var endTimes:Array<Dynamic> ;      // end time instances
        var pastBeginTimes:Array<Dynamic> ;
        var pastEndTimes:Array<Dynamic> ;
        var active:Bool ;            // active time interval
        var neverStarted:Bool ;
        var restart:String ;
        var scheduledDuration:Float;

        // currentBeginTime,currentEndTime define the current active interval
        // when inactive, they define the most recent active interval
        var currentBeginTime:Float ;
        var currentEndTime:Float ;


        public function new(svgRoot:SVGSVGNode, xml:XML, ?original:SVGNode = null){
            
            INDEFINITE = Math.pow(2,32) -1;
            lastDocTime = -1;
            currentRepeatIndex = -1;
            repeatCountSpecified = false;
            repeatDurSpecified = false;
            beginTimeSpecs = new Array();
            endTimeSpecs = new Array();
            beginTimes = new Array();
            endTimes = new Array();
            pastBeginTimes = new Array();
            pastEndTimes = new Array();
            active = false;
            neverStarted = true;
            restart = "always";
            currentBeginTime = -1;
            currentEndTime = INDEFINITE;
            super(svgRoot, xml, original);
        }

        override function onAddedToStage(event:Event):Void {
            super.onAddedToStage(event);
            svgRoot.addEventListener(SVGEvent.SVGLoad, onSVGLoad);
            svgRoot.addEventListener(SVGEvent._SVGDocTimeUpdate, onSVGDocTimeUpdate);
            svgRoot.addEventListener(SVGEvent._SVGDocTimeSeek, onSVGDocTimeSeek);
        }

        override function onRemovedFromStage(event:Event):Void {
            svgRoot.removeEventListener(SVGEvent.SVGLoad, onSVGLoad);
            svgRoot.removeEventListener(SVGEvent._SVGDocTimeUpdate, onSVGDocTimeUpdate);
            svgRoot.removeEventListener(SVGEvent._SVGDocTimeSeek, onSVGDocTimeSeek);
            super.onRemovedFromStage(event);
        }

        function onSVGLoad(evt:Event):Void {
            initialize();
        }
		
        override function onAdded():Void {
            // If we have been added to the SVG tree AFTER it has already been
            // loaded, then we need to immediately initialize ourselves
            if (null != this.svgRoot && this.svgRoot.isLoaded()) {
                initialize();
            }
        }
        
        function initialize():Void {
            // Process the timing parameters
            parseParameters();
        }

        override function drawNode(?event:Event = null):Void {
            if (Std.is( this, SVGVideoNode)) {
                return super.drawNode(event);
            }
            topSprite.visible = false;
            if ( (topSprite.parent != null) && (this._invalidDisplay) ) {
                this._invalidDisplay = false;

                if (this._xml != null) {
                    if (!this._parsedChildren) {
                        this.parseChildren();
                        this._parsedChildren = true;
                    }
                }
                topSprite.removeEventListener(Event.ENTER_FRAME, drawNode);
            }

            if (!this._initialRenderDone && null != topSprite.parent) {
                this._initialRenderDone = true;
                this.svgRoot.renderFinished();
            }
			return null;

        }

        function onSVGDocTimeUpdate(event:Event):Void {
            lastDocTime = cast(event, SVGEvent).docTime;

            var beginInstance:Float;
            var endInstance:Float;
            // Keep looping as long as there is work being done. There may
            // be a backlog of work that needs to be processed. 

            while (true) {
                beginInstance = getBeginInstance();
                endInstance = getEndInstance();
				
				//trace( this.active + ' beginInstance:' + beginInstance + ' endInstance:' + endInstance);
				//if (beginInstance == endInstance) break;
                if (!this.active) {
                    // If not active, process begin instances.

                    // Is the next begin instance ready to run?
                    if (beginInstance <= lastDocTime) {
                        // If restart is 'never', do not start more than one
                        // interval.
                        if ( (restart!="never")  || neverStarted) {
                            timeIntervalStarted(beginInstance);
                            this.active = true;
                        }
                        // Whether we started or not, we have processed this
                        // instance and so we move on to the next.
                        pastBeginTimes.unshift(beginTimes.shift());
                    }
                    else break; // be sure to break anytime we need to wait
                }
                else {
                    // Time interval is active, process end instances.

                    // Discard moot events. These may exist if created
                    // with a negative time offset or if the document
                    // is seeked.

                    // Discard all endTimes up to currentBeginTime.
                    while (endInstance < currentBeginTime) {
                        endInstance = nextEndInstance();
                    }
                    // Discard all beginTimes up to currentBeginTime.
                    while (beginInstance < currentBeginTime) {
                        beginInstance = nextBeginInstance();
                    }

                    // Check for an end before (or at?) the next begin.
                    var tmpEnd:Float = Math.min(endInstance, currentEndTime);
                    if ( (tmpEnd <= lastDocTime) && (tmpEnd <= beginInstance)) {
                        // If the end instance came first, then it is the
                        // end time we are using.
                        if (endInstance <= currentEndTime) {
                            pastEndTimes.unshift(endTimes.shift());
                        }
                        processRepeatIntervals(tmpEnd);
                        timeIntervalEnded(tmpEnd);
                        this.active = false;
                    }
                    else {  //check for a restart
                        if (beginInstance <= lastDocTime) {
                            if (beginInstance >= currentBeginTime) {
                                processRepeatIntervals(beginInstance);
                                if (restart == "always") {
                                    timeIntervalEnded(beginInstance);
                                    timeIntervalStarted(beginInstance);
                                    //this.active = true;
                                }
                            }
                            pastBeginTimes.unshift(beginTimes.shift());
                        }
                        else break; // be sure to break anytime we need to wait
                    }
                }
            }
            if (this.active) processRepeatIntervals(lastDocTime);
        }

        function onSVGDocTimeSeek(event:Event):Void {
            //this.dbg("docTimeSeek: documentTime: " + SVGEvent(event).docTime);
            // To simplify this, we just start over and reprocess instances
            // up to the current time. TODO: We should keep track of
            // what time we have seeked to in order to avoid dispatching
            // events for events which end up in the past.
            while (pastBeginTimes.length > 0) {
                beginTimes.unshift(pastBeginTimes.shift());
            }
			//untyped beginTimes.sortOn(Array.NUMERIC);
			beginTimes.sort(ArrayTools.sortNum);

            while (pastEndTimes.length > 0) {
                endTimes.unshift(pastEndTimes.shift());
            }
            //untyped endTimes.sortOn(Array.NUMERIC);
            endTimes.sort(ArrayTools.sortNum);

        }

        // These are hooks that animations can use to trigger a redraw that would
        // not otherwise occur when there are no remaining active animations.
        function timeIntervalStarted(docTime:Float):Void {
            currentBeginTime = docTime;
            if (scheduledDuration == INDEFINITE) currentEndTime = INDEFINITE;
            else currentEndTime = currentBeginTime + scheduledDuration;
            currentRepeatIndex = 0;
            neverStarted = false;
            var svgEvent:SVGEvent = new SVGEvent(SVGEvent._SVGAnimBegin);
            svgEvent.setDocTime(docTime);
            this.dispatchEvent(svgEvent);
            repeatIntervalStarted(currentBeginTime); // starts media
        }
        
        function timeIntervalEnded(docTime:Float):Void {
            currentEndTime = docTime;
            var svgEvent:SVGEvent = new SVGEvent(SVGEvent._SVGAnimEnd);
            svgEvent.setDocTime(docTime);
            this.dispatchEvent(svgEvent);
        }

        /*
            if active docseek back to currentBeginTime
            else seek to earliest begin instance (forward or back)
            else do a begin at current doctime
        */
        public function getHyperlinkDocTime():Float {
            if (this.active) {
                return currentBeginTime;
            }

            var minTime:Float = INDEFINITE;
            for (i in 0...pastBeginTimes.length) {
                if (pastBeginTimes[i] < minTime) {
                    minTime = pastBeginTimes[i];
                }
            }
            for (i in 0...beginTimes.length) {
                if (beginTimes[i] < minTime) {
                    minTime = beginTimes[i];
                }
            }

            if (minTime != INDEFINITE) {
               return minTime;
            } else {
               return this.svgRoot.getDocTime();
            }
        }

        function repeatIntervalStarted(docTime:Float):Void {
            if (currentRepeatIndex > 0) {
                var svgEvent:SVGEvent = new SVGEvent(SVGEvent._SVGAnimRepeat);
                svgEvent.setDocTime(docTime);
                this.dispatchEvent(svgEvent);
            }
        }

        function repeatIntervalEnded(docTime:Float):Void {
        }

        /**
         *
         * Parsing Methods
         *
         **/

        function parseParameters():Void {
            parseEachRepeatDuration();  // duration
            parseRepeatCount();         // repeatCount
            parseAllRepeatsDuration();  // repeatDuration
            parseMinIntervalDuration(); // min
            parseMaxIntervalDuration(); // max
            parseRestart();             // restart
            parseEndTimeSpecs();        // end
            parseBeginTimeSpecs();      // begin
            setScheduledDuration();
			//trace(id + ' beginTimes:'+ beginTimes.join(', ') );
			//trace('endTimes:'+ endTimes.join(', ') + ':' + INDEFINITE);
        }

        /**
         *
         * Parse the 'begin' parameter.
         *
         **/
        function parseBeginTimeSpecs():Void {
            // If no begin is specified, the spec says the default is zero
            var begin:String = this.getAttribute('begin', '0');
			//trace(begin);
            var timeSpecStrings:Array<Dynamic> = ~/;/g.split(begin);
			//trace('timeSpecStrings:' + timeSpecStrings.toString());

            var timeSpecIndex:UInt =0;
            //for (timeSpecString in Reflect.fields(timeSpecStrings)) {
            for (timeSpecString in timeSpecStrings) {
                var timeSpec:TimeSpec = TimeSpec.parseTimeSpec("begin", timeSpecString, this);
                beginTimeSpecs.push(timeSpec);
				//trace(timeSpecString + ' timeSpec:' + timeSpec + " id:" + id);

                // Simple offsets can resolve an interval right away
                if (Std.is( timeSpec, OffsetTimeSpec)) {
                    addBeginInstance(cast(timeSpec, OffsetTimeSpec).getOffset());
                }
                if (Std.is( timeSpec, EventTimeSpec)) {
                    var targetNode:SVGNode = this.svgRoot.getNode(cast(timeSpec).nodeID);
                    if (null != targetNode) {
                        switch (cast(timeSpec, EventTimeSpec).eventType) {
                            case SVGEvent._SVGAnimBegin, SVGEvent._SVGAnimEnd, SVGEvent._SVGAnimRepeat:
                               targetNode.addEventListener(cast(timeSpec, EventTimeSpec).eventType,
                                                           cast(timeSpec, EventTimeSpec).handleEvent);
                            default: 
                               targetNode.topSprite.addEventListener(cast(timeSpec, EventTimeSpec).eventType,
                                                                     cast(timeSpec, EventTimeSpec).handleEvent);
                        }
                    }
                }
                timeSpecIndex++;
            }
        }

        /**
         *
         * Parse the 'end' parameter.
         * Note that the 'end' parameter refers to the end of
         * all the repeat cycles.
         *
         **/
        function parseEndTimeSpecs():Void {
            // If no end is specified, the spec says the default is indefinite
            var endString:String = this.getAttribute('end', 'indefinite');
			//trace('endString:'+ endString);
            var timeSpecStrings:Array<Dynamic> = ~/;/g.split(endString);
			//trace('timeSpecStrings:' + timeSpecStrings.toString());
            //for (timeSpecString in Reflect.fields(timeSpecStrings)) {
            for (timeSpecString in timeSpecStrings) {
                var timeSpec:TimeSpec = TimeSpec.parseTimeSpec("end", timeSpecString, this);
				//trace(timeSpecString + ' timeSpec:' + timeSpec + " id:" + id);
                if (null != timeSpec) {
                    endTimeSpecs.push(timeSpec);
                    if (Std.is( timeSpec, OffsetTimeSpec)) {
                        addEndInstance(cast(timeSpec, OffsetTimeSpec).getOffset());
                    }
                    if (Std.is( timeSpec, EventTimeSpec)) {
                        var targetNode:SVGNode = this.svgRoot.getNode(cast(timeSpec, EventTimeSpec).nodeID);
                        if (null != targetNode) {
                            switch (cast(timeSpec, EventTimeSpec).eventType) {
                                case SVGEvent._SVGAnimBegin, SVGEvent._SVGAnimEnd, SVGEvent._SVGAnimRepeat:
                                   targetNode.addEventListener(cast(timeSpec, EventTimeSpec).eventType,
                                                               cast(timeSpec).handleEvent);
                                default:
                                   targetNode.topSprite.addEventListener(cast(timeSpec, EventTimeSpec).eventType,
                                                                         cast(timeSpec).handleEvent);
                            }
                        }
                    }
                }
            }
        }

        function parseEachRepeatDuration():Void {
            // If no duration is specified, the spec says the default is indefinite
            var durationString:String = this.getAttribute('dur', 'indefinite');
            if (durationString == 'media' || durationString == 'indefinite') {
                this.eachRepeatDuration =  INDEFINITE;
            }
            else {
                this.eachRepeatDuration =  SVGUnits.parseTimeVal(durationString);
            }
        }

        function parseRepeatCount():Void {
            // If no repeat is specified, the spec says the default is one
            var repeatCountString:String = this.getAttribute('repeatCount', null);
            repeatCountSpecified = repeatCountString != null;
            this.repeatCount = 1;
            if (repeatCountSpecified) {
                if (repeatCountString == 'indefinite') {
                    this.repeatCount = INDEFINITE;
                }
                else {
                    this.repeatCount = SVGUnits.parseNum(repeatCountString);
                }
            }
        }

        function parseAllRepeatsDuration():Void {
            // repeatDuration parsed as allRepeatsDuration
            var allRepeatsDurationString:String = this.getAttribute('repeatDur', null);
            repeatDurSpecified = allRepeatsDurationString != null;
            if (repeatDurSpecified) {
                if (allRepeatsDurationString == 'indefinite') {
                    allRepeatsDuration = INDEFINITE;
                }
                else {
                    allRepeatsDuration = SVGUnits.parseTimeVal(allRepeatsDurationString);
                }
                if (repeatCountSpecified && (eachRepeatDuration != INDEFINITE)) {
                    allRepeatsDuration = Math.min(this.allRepeatsDuration, eachRepeatDuration*repeatCount);
                }
            }
            else allRepeatsDuration = eachRepeatDuration;
        }

        function parseMinIntervalDuration():Void {
            var minString:String = this.getAttribute('min', '0');
            if (minString == 'media') {
                this.minIntervalDuration = 0;
            }
            else {
                this.minIntervalDuration = SVGUnits.parseTimeVal(minString);
                if (Math.isNaN(this.minIntervalDuration)) {
                    this.minIntervalDuration = 0;
                }
                else {
                    this.minIntervalDuration = Math.max(0, this.minIntervalDuration);
                }
            }
        }

        function parseMaxIntervalDuration():Void {
            var maxString:String = this.getAttribute('max', 'indefinite');
            if ( (maxString == 'indefinite') ||
                 (maxString == 'media')) {
                this.maxIntervalDuration = INDEFINITE;
            }
            else {
                this.maxIntervalDuration = SVGUnits.parseTimeVal(maxString);
                if (Math.isNaN(this.maxIntervalDuration) ||
                    (this.maxIntervalDuration < this.minIntervalDuration)) {
                    this.maxIntervalDuration = INDEFINITE;
                }
            }
        }
        
        function parseRestart():Void {
            this.restart = this.getAttribute('restart', 'always');
        }

        /**
         * Called when a node is created after page load with XML content
         * added as a child. Forces a parse.
         */
        override public function forceParse():Void {
            if (this._xml != null) {
                this.parseParameters();
                super.forceParse();
            } 
        }

        public function addBeginInstanceEvent(event:Event, offset:Float):Void {
            var docTime:Float;
            if (Std.is( event, SVGEvent)) docTime = cast(event, SVGEvent).docTime;
            else docTime = this.svgRoot.getDocTime();
            addBeginInstance(docTime + offset);
        }

        public function addEndInstanceEvent(event:Event, offset:Float):Void {
            var docTime:Float;
            if (Std.is( event, SVGEvent)) docTime = cast(event, SVGEvent).docTime;
            else docTime = this.svgRoot.getDocTime();
            addEndInstance(docTime + offset);
        }

        public function addBeginInstance(docTime:Float):Void {
            beginTimes.push(docTime);
           // untyped beginTimes.sortOn(Array.NUMERIC);
            beginTimes.sort(ArrayTools.sortNum);
        }

        public function addEndInstance(docTime:Float):Void {
            endTimes.push(docTime);
            //untyped endTimes.sortOn(Array.NUMERIC);
            endTimes.sort(ArrayTools.sortNum);
        }

        function getBeginInstance():Float {
            if (beginTimes.length > 0)
                return(beginTimes[0]);
            return(INDEFINITE);
        }

        function nextBeginInstance():Float {
            pastBeginTimes.unshift(beginTimes.shift());
            return(getBeginInstance());
        }

        function getEndInstance():Float {
            if (endTimes.length > 0)
                return(endTimes[0]);
            return(INDEFINITE);
        }

        function nextEndInstance():Float {
            pastEndTimes.unshift(endTimes.shift());
            return(getEndInstance());
        }

        function setScheduledDuration():Void {
            if (repeatDurSpecified) scheduledDuration = allRepeatsDuration;
            else if ((eachRepeatDuration == INDEFINITE) || (repeatCount == INDEFINITE))
                scheduledDuration = INDEFINITE;
            else scheduledDuration = eachRepeatDuration * repeatCount;
            scheduledDuration = Math.min(maxIntervalDuration, scheduledDuration);
            scheduledDuration = Math.max(minIntervalDuration, scheduledDuration);
        }

        function processRepeatIntervals(docTime:Float):Void {
            while (currentRepeatIndex < getRepeatIndex(docTime)) {
                var currentRepeatStartTime:Float = this.currentBeginTime + currentRepeatIndex * this.eachRepeatDuration;
                if (currentRepeatIndex >= 0) {
                    repeatIntervalEnded(currentRepeatStartTime);
                }
                repeatIntervalStarted(currentRepeatStartTime);
                currentRepeatIndex++;
            }
        }

        function getRepeatIndex(docTime:Float):Int {
            // current active interval or most recently completed interval
            if (this.eachRepeatDuration == INDEFINITE) return 0;
            docTime = Math.min(docTime, this.currentEndTime);
            var repeatIndex:Int = Math.floor( (docTime - currentBeginTime) / this.eachRepeatDuration);
            var currentRepeatStartTime:Float = this.currentBeginTime + repeatIndex * this.eachRepeatDuration;
            var repeatRunTime:Float = docTime - currentRepeatStartTime;
            if (repeatRunTime == 0 && repeatIndex > 0) {
                return repeatIndex -1;
            }
            else {
                return repeatIndex;
            }
        }

        function getRepeatIntervalFraction(docTime:Float):Float {
            // current active interval or most recently completed interval
            if (this.eachRepeatDuration == INDEFINITE) return 0;
            docTime = Math.min(docTime, this.currentEndTime);
            var currentRepeatStartTime:Float = this.currentBeginTime + this.getRepeatIndex(docTime) * this.eachRepeatDuration;
            var repeatRunTime:Float = docTime - currentRepeatStartTime;

            return Math.min(1.0, repeatRunTime / eachRepeatDuration);
        }

        public function beginElement():Bool {
            return beginElementAt(0);
        }

        public function beginElementAt(offset:Float):Bool {
            // XXX return value doesn't account for processing of pending time instances
            var docTime:Float = this.svgRoot.getDocTime() + offset;
            if (docTime < currentBeginTime) return(false);  // negative offset could cause this
            addBeginInstance(docTime);
            return ( (restart=="always") ||
                     (!this.active && ( (restart!="never")  || neverStarted)));
        }

        public function endElement():Bool {
            return endElementAt(0);
        }

        public function endElementAt(offset:Float):Bool {
            // XXX return value doesn't account for processing of pending time instances
            var docTime:Float = this.svgRoot.getDocTime();
            if ( (docTime + offset) < currentBeginTime) return(false);  // negative offset could cause this
            addEndInstance(docTime + offset);
            return this.active;
        }

    }
